package apibasej.basic.misc.io;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.logging.Level;

import apibasej.basic.config.CommonProperties;
import apibasej.basic.config.ConfigManager;

public class UtilFile extends UtilIO {


	private String regexValidFiles; 
	private boolean addDirs = false;
	private ArrayList<File> filesListed = new ArrayList<File>();
	private File currentFile;
	
	public static String getLineSeparator(){
		return System.getProperty("line.separator");
	}
	public static String getFileSeparator(){
		return System.getProperty("file.separator"); //Character that separates components of a file path. This is "/" on UNIX and "\" on Windows.
	}
	
	public static byte[] readFile(File f) throws IOException{
		try(FileInputStream fis = new FileInputStream(f)){
			byte[] b = new byte[(int)f.length()];
			fis.read(b);
			return b;
		}
	}
	
	
	public String getCurrentPath(){
		return Thread.currentThread().getContextClassLoader().getResource(".").getPath();
	}
	
	public static byte[] readFile(String path) throws IOException{
		return readFile(new File(path));
	}
	
	public static String readFileString(String path) throws IOException{
		return readFileString(new File(path));
	}
	public static String readFileString(String path, String charset) throws IOException{
		return readFileString(new File(path),charset);
	}
	public static String readFileString(File f) throws IOException{
		return readFileString(f,ConfigManager.getPropDefaultForAll(CHARSET));
	}
	public static String readFileString(File f, String charset) throws IOException{
		return new String(readFile(f),charset);
	}
	
	public static ArrayList<String> readLinesFromFile(String path) throws IOException{
		return readLinesFromFile(new File(path));
	}
	public static ArrayList<String> readLinesFromFile(File f) throws IOException{
		return readLinesFromFile(f,ConfigManager.getPropDefaultForAll(CommonProperties.CHARSET));
	}
	public static ArrayList<String> readLinesFromFile(File f, String charset) throws IOException{
		try(FileInputStream fis = new FileInputStream(f);
			InputStreamReader reader = new InputStreamReader(fis, charset);
			//FileReader reader = new FileReader(f);//deste jeito não tem como passar o charset
			BufferedReader leitor = new BufferedReader(reader);
			){
			ArrayList<String> resp = new ArrayList<String>();
			String linha = null;
			while( (linha = leitor.readLine())!=null ){
				resp.add(linha);
			}
			return resp;
		}
	}
	
	public static void writeStringInFile(String s, String path) throws IOException{
		writeStringInFile(s, new File(path), null);
	}
	public static void writeStringInFile(String s, File f) throws IOException{
		writeStringInFile(s, f, null);
	}
	public static void writeStringInFile(String s, File f, String charset) throws IOException{
		if(f.getParentFile()!=null){
			f.getParentFile().mkdirs();// caso não existam as pastas
		}
		/*
		 * !!! não usar assim! por causa do encoding 
		f.createNewFile();//para o PrintWriter o arquivo deve existir 
		try(FileWriter writer = new FileWriter(f);
			PrintWriter saida = new PrintWriter(writer)
			){
			saida.write(s);
			saida.flush();
		}*/
		if(charset==null){
			charset = ConfigManager.getPropDefaultForAll(CHARSET);
			//System.out.println("CHARSET>>>"+charset);
		}
		//!!! o textpad mostra errado !!! cuidado para não confundir
		
		//qual é melhor ???
		try(FileOutputStream fos = new FileOutputStream(f)){
			fos.write(s.getBytes(charset));
			fos.flush();
		}
		/*
		try(FileOutputStream fos = new FileOutputStream(f);
			OutputStreamWriter osw = new OutputStreamWriter(fos,charset);
			Writer out = new BufferedWriter(osw);){
		    out.write(s);
		}*/
	}
	
	public static void writeBytesInFile(byte[] b, String path) throws IOException{
		writeBytesInFile(b, new File(path));
	}
	public static void writeBytesInFile(byte[] b, File f) throws IOException{
		writeBytesInFile(b, f, false);
	}
	public static void writeBytesInFile(byte[] b, File f, boolean append) throws IOException{
		if(f.getParentFile()!=null){
			f.getParentFile().mkdirs();// para caso não existam as pastas
		}
		try(FileOutputStream fos = new FileOutputStream(f,append)){
			fos.write(b);
			fos.flush();
		}
	}
	
	
	public static void deleteEmptyDirs(File rootDir){
		boolean again = true;
		while(again){
			again = false;
			ArrayList<File> dirs = new ArrayList<>();
			readFilesRecursive(rootDir, dirs, false, true);
			for(File dir : dirs){
				String[] sub = dir.list();
				if(sub==null || sub.length==0){
					boolean ok = dir.delete();
					if(ok){// se apagar algum dir roda tudo novamente, pois podem ter pastas pais para apagar na próxima iteração
						again = true;
					}
				}
			}
		}
	}
	
	public static ArrayList<File> readFilesRecursive(File rootDir){
		ArrayList<File> filesRead = new ArrayList<>();
		readFilesRecursive(rootDir, filesRead, true, false);
		return filesRead;
	}
	public static void readFilesRecursive(File rootDir, ArrayList<File> filesRead, boolean addFiles, boolean addDirs){ // String[] includeExtensions, String[] ignore
		File[] listDirs = rootDir.listFiles();
		if(listDirs!=null){
			for (File ff : listDirs){
				if (ff.isDirectory()){
					if(addDirs){
						filesRead.add(ff);
					}
					readFilesRecursive(ff, filesRead, addFiles, addDirs); // , includeExtensions, ignore
				}else if(addFiles){
					filesRead.add(ff);
				}
			}
		}
	}
	
	
	
	// receber: File.listRoots() ou dir.listFiles() ...
	public void readFilesDirs(File[] listDirs){ // String[] includeExtensions, String[] ignore
		if(listDirs!=null){
			for (File ff : listDirs){
				setCurrentFile(ff);
				if( verifyPathToAdd(ff) ) {
					eventAddFile(ff);
					if ( ff.isDirectory() ) readFilesDirs( ff.listFiles() ); // , includeExtensions, ignore
				}
				setCurrentFile(null);
			}
		}
	}
	
	public void eventAddFile(File f){
		if( (isAddDirs() || !f.isDirectory()) && getFilesListed()!=null ) getFilesListed().add(f);
	}
	
	public boolean verifyPathToAdd(File f){
		if(getRegexValidFiles()==null) {
			return true;
		} else {
			//System.out.println("f.getPath()>"+f.getPath()+" - "+f.getPath().matches(getRegexValidFiles()));
			return f.getPath().matches(getRegexValidFiles());
		}
	}
	
	
	/*
	//[Old technique (pre JDK1.4)]
	public void copyFileStream(File in, File out) throws Exception {
		FileInputStream fis = new FileInputStream(in);
		FileOutputStream fos = new FileOutputStream(out);
		try {
			byte[] buf = new byte[1024];
			int i = 0;
			while ((i = fis.read(buf)) != -1) {
				fos.write(buf, 0, i);
				eventReadInputStream(buf, 0, i);
			}
		} catch (Exception e) {
			throw e;
		} finally {
			if (fis != null)
				fis.close();
			if (fos != null)
				fos.close();
		}
	}
	*/
	
	public static void copyFile(File in, File out) throws IOException {
		copyFile(in, out, false);
	}
	
	public static void copyFile(File in, File out, boolean ignoreExistentNotModified) throws IOException {
		
		//para ignorar a cópia se o destino existe e tem o mesmo tamanho e data de modificação
		if(ignoreExistentNotModified 
				&& out.exists() 
				&& out.length()==in.length() 
				&& out.lastModified()==in.lastModified()){
			return;
		}
		
		if(in.isDirectory()){
			out.mkdirs();
		}else if(in.length() > 1042*1024*50){// 50 MB
			// se for maior que 50 MB copia com stream para não dar OutOfMemory ...
			//copyBigFile(in, out); ////Files.copy(source, out);???
			out.getParentFile().mkdirs();// para criar as pastas acima, senão da erro ao criar o arquivo 
			try(FileInputStream fis = new FileInputStream(in);
				FileOutputStream fos = new FileOutputStream(out);
				){
				byte[] buf = new byte[1024*1024]; // 1 MB
				int len;
				while ( (len = fis.read(buf)) > 0) {
					fos.write(buf, 0, len);
				}
				out.setLastModified(in.lastModified());
			}
		}else{
			out.getParentFile().mkdirs();// para criar as pastas acima, senão da erro ao criar o arquivo 
			// rever esta impl
			// [JDK1.4 using the java.nio package (faster)??]
			// desta forma da erro de memória para arquivos grandes por isso deve verificar tamanho do arquivo (? tertar novamente...)
			try(
				FileInputStream fis = new FileInputStream(in);
				FileChannel inChannel = fis.getChannel();
				FileOutputStream fos = new FileOutputStream(out);
				FileChannel outChannel = fos.getChannel();
				){
				//inChannel.map(FileChannel.MapMode.READ_WRITE, 0, 0);
				inChannel.transferTo(0, inChannel.size(), outChannel);
			}
		}
		//setar por padrão!!! se não quiser setar criar uma flag para ignorar!!!
		out.setLastModified(in.lastModified());
	}
	
	
	public static boolean deleteRecursive(File f){
		if(f.isDirectory()){ // tenta deletar os filhos primeiro
			for(File filho : f.listFiles()){
				deleteRecursive(filho);
			}
		}
		return deleteForced(f);
	}
	
	public static boolean deleteForced(File f){
		boolean del = f.delete();
		for(int i=0; (i<10 && !del); i++) {// para tentar de novo se não conseguir deletar
			try {Thread.sleep(20);} catch (InterruptedException e) {}
			del = f.delete();
			f.deleteOnExit();
		}
		return del;
	}
	
	/*
	public void saveObjInFile(String path, Object obj) throws FileNotFoundException, IOException{
    	ObjectOutput out = new ObjectOutputStream(new FileOutputStream( path ));
        out.writeObject( obj );
        out.close();
	}*/
	/*
	public Object readObjFromFile(String path) throws IOException, ClassNotFoundException{
		 ObjectInputStream in = new ObjectInputStream(new FileInputStream(path));
		 // Deserialize the object
		 Object obj = in.readObject();
		 in.close();
		 return obj;
	}*/

	
	public static Object readObjFromBytes(byte[] bytesObj) throws IOException, ClassNotFoundException{
		try(ByteArrayInputStream is = new ByteArrayInputStream(bytesObj);
			ObjectInputStream in = new ObjectInputStream(is);
			){
			// Deserialize the object
			return in.readObject();
		}
	}
	
	public static byte[] serializeObjToBytes(Object obj) throws IOException{ //, String contentType
	    // Serialize to a byte array
	    try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
	    	ObjectOutput out = new ObjectOutputStream(bos);
	    	){
		    out.writeObject(obj);
		    
			//???
			//chama estes flushs e close para evitar o erro quando for ler: java.io.EOFException ... at java.io.ObjectInputStream$BlockDataInputStream.peekByte(Unknown Source)... (http://stackoverflow.com/questions/15171184/objectinputstream-throws-eofexception)
			try{out.flush();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro out.flush()", t);}
			try{bos.flush();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro fos.flush()", t);}
			try{out.close();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro out.close()", t);}
			try{bos.close();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro fos.close()", t);}
		    
		    // Get the bytes of the serialized object
		    return bos.toByteArray();//bos.toString(contentType).getBytes(contentType);
	    }
	}
	/*
	public static Object readObjFromFile(File f) throws IOException, ClassNotFoundException{
		try(FileInputStream fis = new FileInputStream(f);
			ObjectInputStream in = new ObjectInputStream(fis);
			){
			
			// Deserialize the object
			return in.readObject();
		}
	}
	public static void saveObjInFile(File f, Object obj) throws FileNotFoundException, IOException{
		try(FileOutputStream fos = new FileOutputStream(f);
			ObjectOutput out = new ObjectOutputStream(fos);
			){
			out.writeObject(obj);
			
			//???
			//chama estes flushs e close para evitar o erro quando for ler: java.io.EOFException ... at java.io.ObjectInputStream$BlockDataInputStream.peekByte(Unknown Source)... (http://stackoverflow.com/questions/15171184/objectinputstream-throws-eofexception)
			try{out.flush();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro out.flush()", t);}
			try{fos.flush();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro fos.flush()", t);}
			try{out.close();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro out.close()", t);}
			try{fos.close();}catch(Throwable t){ConfigManager.logDefAll(Level.WARNING, "Erro fos.close()", t);}
		}
	}*/
	
	
	
	public ArrayList<File> getFilesListed() {
		return filesListed;
	}

	public void setFilesListed(ArrayList<File> filesListed) {
		this.filesListed = filesListed;
	}

	public String getRegexValidFiles() {
		return regexValidFiles;
	}

	public void setRegexValidFiles(String regexValidFiles) {
		this.regexValidFiles = regexValidFiles;
	}

	public boolean isAddDirs() {
		return addDirs;
	}

	public void setAddDirs(boolean addDirs) {
		this.addDirs = addDirs;
	}

	public File getCurrentFile() {
		return currentFile;
	}

	public void setCurrentFile(File currentFile) {
		this.currentFile = currentFile;
	}
	
	
	
	
	public static File getTempDir(){
		String t = System.getProperty("java.io.tmpdir");
		if(t!=null){
			File f = new File(t);
			if(f.exists()){
				return f;
			}
		}
		t = System.getenv("TEMP");
		if(t!=null){
			File f = new File(t);
			if(f.exists()){
				return f;
			}
		}
		File f = new File("/tmp/");
		f.mkdirs();
		return f;
	}
	public static File getHomeDir(boolean returnCurrentIfNotFound) throws FileNotFoundException{
		String uh = System.getProperty("user.home");
		if(uh==null){
			if(returnCurrentIfNotFound){
				uh = ".";
			}else{
				throw new NullPointerException("System.getProperty(\"user.home\")==null");
			}
		}
		if(!new File(uh).exists() && returnCurrentIfNotFound){
			uh = ".";//se não achar retorna o diretório atual ???
		}
		File home = new File(uh);
		if(!home.exists()){
			throw new FileNotFoundException("User Home not found: "+home.getAbsolutePath());
		}
		return home;
	}
	public static File getDesktopDir(boolean returnCurrentIfNotFound) throws FileNotFoundException{
		File h = getHomeDir(returnCurrentIfNotFound);
		File desktop = null;
		for(File f : h.listFiles()){
			if(f.getName().equalsIgnoreCase("Área de Trabalho")
					|| f.getName().equalsIgnoreCase("Desktop")){
				desktop = f;
				break;
			}
		}
		if(desktop==null){
			if(returnCurrentIfNotFound){
				desktop = new File("."); //se não achar retorna o diretório atual ???
			}else{
				throw new FileNotFoundException("Desktop not found in user.home: "+h.getAbsolutePath());
			}
		}
		if(!desktop.exists()){//não deve ocorrer
			throw new FileNotFoundException("Desktop not found: "+desktop.getAbsolutePath());
		}
		return desktop;
	}
	
}
